"""
PyMak rule and action to build Python files. This module is built on
the PyMak version of pymak.

The action allows for the target to vary by the 'py' variant. If it
does, it will use its value to determine which version of python the
files will be compiled with.

This module is designed to be imported using import_pymak_extension
method on the pymak context. This means that the module ought to
support an init() and a version_ok() method.
"""

# Python imports
import os
import py_compile
import zipfile
import sys
import logging
import shutil

# PyMak imports
import pymak

MAJOR_VERSION = 1
MINOR_VERSION = 0

def init(context):
    """This method sets up into the provided context the following:

    - aliases the '.py' to the 'null' rule.

    - binds a PyZipRule instance to '.pyzip'

    - binds a PyZipAction instance to '.zip.py'

    - defines the property 'PYTHON_HOME' dependent on the 'py' variant.

    - adds the variants 'py' and 'style' into the context, defaulted
    to '2.3' and 'dbg' respectively.
    """
    #context.alias_rule('.py', 'null') # by default, nothing needed to make .py file
    context.declare_rule('.py', PyRule(context))
    context.declare_rule(".pyzip", PyZipRule(context))
    context.declare_rule(".pyczip", PycZipRule(context))
    context.declare_rule(".egg", PyEggRule(context))
    
    context.declare_action(".zip.py", PyZipAction())
    context.declare_action(".pyc.py", PyAction())
    context.declare_action(".zip.pyc", PycZipAction())
    context.declare_action(".egg.py", PyEggAction())
    context.declare_action(".egg.install", PyEggInstall())

    context.define_property("PYTHON_HOME", [ ("py", "2.3") ], "$(PYTHON_2_3)")
    context.define_property("PYTHON_HOME", [ ("py", "2.4") ], "$(PYTHON_2_4)")
    context.define_property("PYTHON_HOME", [ ("py", "2.5") ], "$(PYTHON_2_5)")
    context.define_property("PYTHON_HOME", [ ("py", "2.6") ], "$(PYTHON_2_6)")

    context.set_variants_from_dict(dict(py=('2.3', '2.4', '2.5', '2.6'),
                                        style=('dbg', 'rel')))


def version_ok(major, minor):
    if major != MAJOR_VERSION:
        return False
    if minor > MINOR_VERSION:
        return False
    return True


class PyZipRule(pymak.Rule):
    """This rule works with the PyZipAction to generate a zip file of
    pyc files from a dependent list of targets which must be .py
    files. This zip file can then be used in the same way as jar files
    are used in the java world.

    Typical usage would be:

    package_zip = pmc.target('mypackage.zip',
                             rule='.pyzip',
                             depends_on=pymak.path('srcdir').walk('*.py'))

    The path_depth parameter sets the default for the path depth used
    for each dependent target (the dependent target can override this
    value if it has a 'path_depth attribute on the target.
    """
    def __init__(self, context, path_depth=1, package_source=True):
        super(PyZipRule, self).__init__(context)
        self.path_depth = path_depth
        self.package_source = package_source

    def apply_rule(self, tpath, **kwargs):
        variants = kwargs.pop('variants', [])
        variants += ['py', 'style']
        attrs = dict(package_source=self.package_source,
                     default_path_depth=self.path_depth)
        attrs.update(kwargs.pop('attrs', {}))
        return self.context.target(tpath,
                                   rule=None,
                                   variants=variants,
                                   action='.zip.py',
                                   target_dir="build/pyzip/$(allvariants)",
                                   attrs=attrs,
                                   **kwargs)



class PyZipAction(pymak.Action):
    """This action is used by the '.pyzip' rule (PyZipRule). There
    should rarely be a need to interact with this class directly in
    PyMak files.
    """
    def __init__(self, package_source=True):
        pymak.Action.__init__(self)
        self.package_source = package_source
    
    def execute_action(self, target, variant_values):

        package_source = getattr(target, 'package_source', True)

        logging.info("%s...", target.path)
        logging.debug(_("Compiling python zip file '%s' from %s"), target.path, target._depends_on.string_of_target_files(variant_values))

        py_ver ="2.3"
        if target._implements_variants.contains('py'):
            py_ver = target._implements_variants.lookup('py')

        z = zipfile.ZipFile(target.path, "w")
        try:

            # the path_depth describes how many elements in the dependent target
            # path are going to be included in the archive name.
            default_path_depth = getattr(target, 'default_path_depth', 1)

            for t in target._depends_on.list_of_sub_targets(variant_values):
                tfile = t.path
                keep_parts = getattr(t, 'path_depth', default_path_depth)
                arcname = pymak.path("").joinpath(*tfile.splitall()[- keep_parts:])

                if tfile.ext == ".py":
                    logging.warning("%s...", tfile)
                    # compile the .py file
                    _compile_file(tfile, py_ver, variant_values, target)

                    # zip the .pyc file
                    print "ADDING ARCHIVE", arcname + 'c'
                    z.write(tfile + 'c', str(arcname + 'c'))

                    # delete the pyc file
                    os.remove(tfile + 'c')

                    # zip the .py file, required for py2exe, which is a windows
                    # only thing
                    if package_source and pymak.os_is_windows():
                        arcname = pymak.to_path("src") / arcname
                        z.write(tfile, str(arcname))

                else:
                    z.write(t.path, str(arcname))
        finally:
            # always close this so that failures don't leave a file handle
            # open on the file.
            z.close()

#
# -----------------
#

class PyRule(pymak.Rule):
    """TODO
    """

    def apply_rule(self, tpath, **kwargs):
        variants = kwargs.pop('variants', [])
        variants += ['py', 'style']
        py_t = self.context.target(tpath,
                                   variants=variants,
                                   rule=None,
                                   **kwargs)
        depth = len(tpath.splitall()) - 1 # splitall will have an empty path element at the start for relative paths

        pyc_t = self.context.target(pymak.path("build/pyc/$(allvariants)") / tpath + 'c',
                                    rule=None,
                                    variants=variants,
                                    action='.pyc.py',
                                    attrs=dict(path_depth=depth),
                                    depends_on=py_t)
        return pyc_t

        
    

class PyAction(pymak.Action):
    """TODO
    """

    def execute_action(self, target, variant_values):
        logging.warning("%s...", target.path)
        t = target._depends_on.list_of_sub_targets(variant_values)[0]
        tfile = t.path
        py_ver = target._implements_variants.lookup('py')
        _compile_file(tfile, py_ver, variant_values, target)
        tfile += 'c'
        shutil.move(tfile, target.path)

class PycZipRule(pymak.Rule):
    """TODO
    """
    
    def apply_rule(self, tpath, **kwargs):
        variants = kwargs.pop('variants', [])
        variants += ['py', 'style']
        return self.context.target(tpath,
                                   rule=None,
                                   variants=variants,
                                   action='.zip.pyc',
                                   target_dir="build/pyzip/$(allvariants)",
                                   **kwargs)

class PycZipAction(pymak.Action):
    """TODO
    """
    
    def execute_action(self, target, variant_values):
        logging.warning("%s...", target.path)
        logging.debug(_("Compiling python zip file '%s' from %s"), target.path, target._depends_on.string_of_target_files(variant_values))

        package_source = getattr(target, 'package_source', True)
        default_path_depth = getattr(target, 'default_path_depth', 1)

        z = zipfile.ZipFile(target.path, "w")
        try:
            for t in target._depends_on.list_of_sub_targets(variant_values):
                tfile = t.path
                keep_parts = getattr(t, 'path_depth', default_path_depth)
                arcname = pymak.path("").joinpath(*tfile.splitall()[- keep_parts:])

                if tfile.ext == ".pyc":
                    if package_source:
                        source_t = t._depends_on.list_of_sub_targets(variant_values)
                        if source_t:
                            source_t = source_t[0]
                            if source_t.path.endswith('.py'):
                                z.write(source_t.path, str(arcname)[:-1])
                
                z.write(tfile, str(arcname))
        finally:
            # always close this so that failures don't leave a file handle
            # open on the file.
            z.close()



#
# -----------------
#

class PyEggRule(pymak.Rule):
    def apply_rule(self, tpath, **kwargs):
        variants = kwargs.pop('variants', [])
        variants += ['py']
        attrs = kwargs.pop('attrs', {})
        setup_attrs = attrs.get('setup_attrs')
        version = setup_attrs.get('version')
        name=setup_attrs.get('name')
        return self.context.target("%s-%s-py$(py).egg" % (name, version),
                                   rule=None,
                                   force_rebuild=True,
                                   variants=variants,
                                   action='.egg.py',
                                   target_dir="build/egg/$(allvariants)",
                                   attrs=attrs,
                                   **kwargs)

class PyEggAction(pymak.Action):
    def execute_action(self, target, variant_values):
        logging.warning("%s...", target.path)
        logging.debug(_("Creating egg '%s'"), target.path)
        setup_attrs = getattr(target, 'setup_attrs', {})
        fname = pymak.to_path('build') / 'egg' / 'setup.py'
        fname.write_text("""
from setuptools import setup
setup(**%s)
""" % str(setup_attrs))
        command = "\"$(PYTHON_HOME)/python\" %s bdist_egg --dist-dir=\"%s\"" % (fname.replace('\\', '/'), str(target.path.parent))
        command = pymak.expand_variables(command, variant_values, target._context, target)
        outanderr = pymak.exec_command(command)


class PyEggInstall(pymak.Action):
    def execute_action(self, target, variant_values):
        egg = target._depends_on.one_target(variant_values)
        logging.warning(_("Installing egg '%s'"), egg.path)
        command = "\"$(PYTHON_HOME)/Scripts/easy_install\" \"%s\"" % str(egg.path)
        command = pymak.expand_variables(command, variant_values, target._context, target)
        outanderr = pymak.exec_command(command)
    

#
# -----------------
#


#
# Compile the supplied Python file with the specified
# version of Python. It it's the same version running
# this code, then the compile is done 'inline', otherwise
# the appropriate version of python is run.
#
def _compile_file(fname, py_ver, variant_values, target):
    if sys.version.startswith(py_ver):
        # we can compile 'inline'
        try:
            py_compile.compile(fname, doraise=True)
        except py_compile.PyCompileError, e:
            raise pymak.CommandFailedException("Compiling %s" % fname, str(e))
    else:
        # invoke the right version of python to compile it
        command = "\"$(PYTHON_HOME)/python\" -c \"import compiler; compiler.compileFile('%s')\" " % fname.replace('\\', '/')
        command = pymak.expand_variables(command, variant_values, target._context, target)
        outanderr = pymak.exec_command(command)

#
# -----------------------------------------------------------
#


